"""
Console-based user interface for the data preprocessing pipeline.

This module provides a comprehensive console interface for interacting with
the preprocessing pipeline, including file selection, configuration, progress
tracking, and results display.
"""

import os
import sys
import time
from threading import Event
from typing import Optional, Dict, List
from pathlib import Path

from core.file_manager import FileManager
from core.data_loader import DataLoader
from core.configuration_manager import ConfigurationManager
from core.combination_generator import CombinationGenerator
from core.processing_pipeline import ProcessingPipeline, BatchProcessingResult
from core.output_manager import OutputManager
from core.models import ProcessingConfiguration, ProgressInfo
from core.error_handler import get_error_handler, ErrorContext
from core.exceptions import (
    PreprocessingPipelineError,
    UserCancellationError,
    FileOperationError,
    ConfigurationError,
    ProcessingError
)
from preprocessors.registry import PreprocessorRegistry
from utils.progress_tracker import ProgressTracker


class ConsoleUI:
    """
    Console-based user interface for the data preprocessing pipeline.
    
    This class provides a complete console interface for users to interact
    with the preprocessing pipeline, from file selection to results display.
    """
    
    def __init__(self, file_manager=None, data_loader=None, config_manager=None,
                 combination_generator=None, output_manager=None, 
                 processing_pipeline=None, preprocessor_registry=None):
        """
        Initialize the console UI with required components.
        
        Args:
            file_manager: FileManager instance (creates new if None)
            data_loader: DataLoader instance (creates new if None)
            config_manager: ConfigurationManager instance (creates new if None)
            combination_generator: CombinationGenerator instance (creates new if None)
            output_manager: OutputManager instance (creates new if None)
            processing_pipeline: ProcessingPipeline instance (creates new if None)
            preprocessor_registry: PreprocessorRegistry instance (creates new if None)
        """
        # Use provided components or create new ones
        self.file_manager = file_manager or FileManager()
        self.data_loader = data_loader or DataLoader()
        self.config_manager = config_manager or ConfigurationManager()
        self.combination_generator = combination_generator or CombinationGenerator()
        self.output_manager = output_manager or OutputManager()
        
        # Initialize preprocessor registry with auto-discovery if not provided
        if preprocessor_registry is None:
            self.preprocessor_registry = PreprocessorRegistry()
            self.preprocessor_registry.auto_discover_processors()
        else:
            self.preprocessor_registry = preprocessor_registry
        
        # Initialize processing pipeline if not provided
        if processing_pipeline is None:
            self.processing_pipeline = ProcessingPipeline(self.preprocessor_registry)
        else:
            self.processing_pipeline = processing_pipeline
        
        # UI state
        self.current_data = None
        self.current_config = None
        self.cancellation_event = Event()
        
    def run(self) -> None:
        """
        Run the main console interface.
        
        Provides the main menu system for file selection and configuration.
        
        Requirements: 1.5, 2.4, 3.4 - main menu system for file selection and configuration
        """
        try:
            self._display_welcome()
            
            while True:
                try:
                    choice = self._display_main_menu()
                    
                    if choice == '1':
                        self._handle_file_selection()
                    elif choice == '2':
                        self._handle_configuration()
                    elif choice == '3':
                        self._handle_processing()
                    elif choice == '4':
                        self._display_system_info()
                    elif choice == '5':
                        self._display_help()
                    elif choice == '6':
                        self._handle_exit()
                        break
                    else:
                        print("Invalid choice. Please select a number from 1-6.")
                        
                except KeyboardInterrupt:
                    print("\n\nOperation cancelled by user.")
                    if self._confirm_exit():
                        break
                except Exception as e:
                    print(f"\nError: {e}")
                    print("Please try again or contact support if the problem persists.")
                    
        except Exception as e:
            print(f"Critical error: {e}")
            print("The application will now exit.")
        finally:
            self._cleanup()
    
    def _display_welcome(self) -> None:
        """Display welcome message and system information."""
        print("=" * 70)
        print("    数据预处理管道系统")
        print("    灵活的Excel数据处理工具")
        print("=" * 70)
        print()
        
        # Display available preprocessors
        method_names = self.preprocessor_registry.get_method_names()
        print(f"可用的预处理方法: {len(method_names)}种")
        # 按ID排序显示
        for method_id in sorted(method_names.keys()):
            name = method_names[method_id]
            print(f"  {method_id}: {name}")
        print()
    
    def _display_main_menu(self) -> str:
        """
        Display main menu and get user choice.
        
        Returns:
            str: User's menu choice
        """
        print("-" * 50)
        print("主菜单")
        print("-" * 50)
        print("1. 选择Excel文件")
        print("2. 配置处理方式")
        print("3. 开始处理")
        print("4. 系统信息")
        print("5. 帮助")
        print("6. 退出")
        print()
        
        # Show current status
        if self.current_data is not None:
            print(f"✓ 已加载文件: {self.current_data.shape[0]}行, {self.current_data.shape[1]}列")
        else:
            print("○ 未加载文件")
            
        if self.current_config is not None:
            total_combinations = self.combination_generator.calculate_total_combinations(
                self.current_config.layer_selections
            )
            print(f"✓ 已配置处理: {self.current_config.layer_count}层, {total_combinations}种组合")
        else:
            print("○ 未配置处理")
        
        print()
        return input("请选择选项 (1-6): ").strip()
    
    def _handle_file_selection(self) -> None:
        """
        Handle Excel file selection and loading.
        
        Requirements: 1.1, 1.2, 1.3, 1.4, 1.5 - file selection and data validation
        """
        print("\n" + "=" * 50)
        print("FILE SELECTION")
        print("=" * 50)
        
        with ErrorContext("file selection", suppress_errors=True):
            # Select file
            print("准备选择Excel文件...")
            print("支持的格式: .xlsx, .xls")
            print("数据格式要求: 第一列为ID，其余列为数值数据")
            print()
            
            file_path = self.file_manager.select_file()
            
            if not file_path:
                print("未选择文件，返回主菜单")
                return
            
            print(f"\n✓ 已选择文件: {file_path}")
            
            # Load and validate data
            print("正在加载Excel数据...")
            data = self.data_loader.load_excel(file_path)
            
            if data is None:
                print("Failed to load Excel file.")
                return
            
            print("Validating data structure...")
            if not self.data_loader.validate_data_structure(data):
                print("Data validation failed.")
                return
            
            # Store loaded data
            self.current_data = data
            
            # Display data preview
            self._display_data_preview(data, file_path)
            
            print("✓ File loaded successfully!")
    
    def _display_data_preview(self, data, file_path: str) -> None:
        """
        Display a preview of the loaded data.
        
        Args:
            data: Loaded DataFrame
            file_path (str): Path to the loaded file
            
        Requirements: 1.5 - display data preview
        """
        print("\n" + "-" * 40)
        print("DATA PREVIEW")
        print("-" * 40)
        print(f"File: {Path(file_path).name}")
        print(f"Dimensions: {data.shape[0]} rows × {data.shape[1]} columns")
        print(f"Columns: {list(data.columns)}")
        print()
        
        # Show first few rows
        print("First 5 rows:")
        print(data.head().to_string(index=False))
        print()
        
        # Show data types
        print("Data types:")
        for col, dtype in data.dtypes.items():
            print(f"  {col}: {dtype}")
        print()
    
    def _handle_configuration(self) -> None:
        """
        Handle processing configuration setup.
        
        Requirements: 2.1, 2.2, 2.3, 2.4, 3.1, 3.2, 3.3, 3.5 - configuration management
        """
        print("\n" + "=" * 50)
        print("PROCESSING CONFIGURATION")
        print("=" * 50)
        
        if self.current_data is None:
            print("Please select and load an Excel file first.")
            return
        
        with ErrorContext("configuration setup", suppress_errors=True):
            # Create output directory
            input_filename = "processed_data"  # Default name
            output_dir = self.output_manager.create_output_directory(input_filename)
            
            # Get available methods and their names
            available_methods = self.preprocessor_registry.get_available_methods()
            method_names = self.preprocessor_registry.get_method_names()
            
            # Create configuration through user interaction
            config = self.config_manager.create_configuration(
                input_file="loaded_data",  # Placeholder since we have data in memory
                output_directory=output_dir,
                available_methods=available_methods,
                method_names=method_names
            )
            
            self.current_config = config
            
            # Display configuration summary
            self._display_configuration_summary(config)
    
    def _display_configuration_summary(self, config: ProcessingConfiguration) -> None:
        """
        Display a summary of the current configuration.
        
        Args:
            config (ProcessingConfiguration): Configuration to display
        """
        print("\n" + "-" * 40)
        print("CONFIGURATION SUMMARY")
        print("-" * 40)
        print(f"Layers: {config.layer_count}")
        print(f"Output directory: {config.output_directory}")
        print()
        
        # Display layer selections with method names
        method_names = self.preprocessor_registry.get_method_names()
        for i, layer_methods in enumerate(config.layer_selections, 1):
            method_name_list = [method_names.get(mid, f"Unknown({mid})") for mid in layer_methods]
            print(f"Layer {i}: {', '.join(method_name_list)}")
        
        # Calculate and display total combinations
        total_combinations = self.combination_generator.calculate_total_combinations(
            config.layer_selections
        )
        print(f"\nTotal combinations: {total_combinations}")
        print()
    
    def _handle_processing(self) -> None:
        """
        Handle the main processing workflow.
        
        Requirements: 7.1, 7.2, 7.3, 7.4 - progress display and user feedback during processing
        """
        print("\n" + "=" * 50)
        print("PROCESSING")
        print("=" * 50)
        
        if self.current_data is None:
            print("Please select and load an Excel file first.")
            return
        
        if self.current_config is None:
            print("Please configure processing settings first.")
            return
        
        try:
            # Generate combinations
            print("Generating processing combinations...")
            combinations = self.combination_generator.generate_combinations(
                self.current_config.layer_selections
            )
            
            print(f"Generated {len(combinations)} combinations to process.")
            
            # Confirm processing start
            if not self._confirm_processing_start(len(combinations)):
                print("Processing cancelled.")
                return
            
            # Reset cancellation event
            self.cancellation_event.clear()
            
            # Start processing with progress tracking
            self._execute_batch_processing(combinations)
            
        except KeyboardInterrupt:
            print("\nProcessing cancelled by user.")
        except Exception as e:
            print(f"Error during processing: {e}")
    
    def _confirm_processing_start(self, combination_count: int) -> bool:
        """
        Confirm with user before starting processing.
        
        Args:
            combination_count (int): Number of combinations to process
            
        Returns:
            bool: True if user confirms, False otherwise
        """
        print(f"\nReady to process {combination_count} combinations.")
        print("This may take some time depending on your data size and number of combinations.")
        print("You can cancel processing at any time by pressing Ctrl+C.")
        print()
        
        while True:
            response = input("Do you want to start processing? (y/n): ").strip().lower()
            if response in ['y', 'yes']:
                return True
            elif response in ['n', 'no']:
                return False
            else:
                print("Please enter 'y' for yes or 'n' for no.")
    
    def _execute_batch_processing(self, combinations: List[List[int]]) -> None:
        """
        Execute batch processing with progress tracking.
        
        Args:
            combinations (List[List[int]]): Combinations to process
            
        Requirements: 7.1, 7.2 - progress display during processing
        """
        # Initialize progress tracker
        progress_tracker = ProgressTracker(
            total_combinations=len(combinations),
            update_callback=self._display_progress_update
        )
        
        print("\nStarting batch processing...")
        print("Press Ctrl+C to cancel processing at any time.")
        print()
        
        # Start progress tracking
        progress_tracker.start_tracking()
        
        # Custom progress callback for the processing pipeline
        def progress_callback(current: int, total: int, combination_id: str):
            # Calculate processing time for the previous combination
            processing_time = None
            if current > 1:
                processing_time = progress_tracker.get_average_processing_time()
            
            progress_tracker.update_progress(combination_id, processing_time)
        
        try:
            # Execute batch processing
            batch_result = self.processing_pipeline.batch_process(
                data=self.current_data,
                combinations=combinations,
                progress_callback=progress_callback,
                cancellation_event=self.cancellation_event
            )
            
            # Save successful results
            self._save_processing_results(batch_result)
            
            # Display final results
            self._display_processing_results(batch_result, progress_tracker)
            
        except KeyboardInterrupt:
            print("\n\nProcessing cancelled by user.")
            self.cancellation_event.set()
        except Exception as e:
            print(f"\nError during batch processing: {e}")
    
    def _display_progress_update(self, progress_info: ProgressInfo) -> None:
        """
        Display progress update during processing.
        
        Args:
            progress_info (ProgressInfo): Current progress information
            
        Requirements: 7.1, 7.2 - real-time progress updates
        """
        # Clear the current line and display progress
        progress_display = self._format_progress_display(progress_info)
        print(f"\r{progress_display}", end="", flush=True)
        
        # Add newline every 10 combinations for better readability
        if progress_info.current_combination % 10 == 0:
            print()  # New line
    
    def _format_progress_display(self, progress_info: ProgressInfo) -> str:
        """
        Format progress information for console display.
        
        Args:
            progress_info (ProgressInfo): Progress information to format
            
        Returns:
            str: Formatted progress string
        """
        # Progress bar
        bar_width = 20
        filled_width = int((progress_info.percentage / 100.0) * bar_width)
        progress_bar = "█" * filled_width + "░" * (bar_width - filled_width)
        
        # Format time remaining
        time_remaining = self._format_time_duration(progress_info.estimated_time_remaining)
        
        # Build progress string
        return (
            f"[{progress_bar}] {progress_info.percentage:5.1f}% "
            f"({progress_info.current_combination}/{progress_info.total_combinations}) "
            f"ETA: {time_remaining:>8} | {progress_info.current_combination_id}"
        )
    
    def _format_time_duration(self, seconds: float) -> str:
        """
        Format time duration in human-readable format.
        
        Args:
            seconds (float): Duration in seconds
            
        Returns:
            str: Formatted time string
        """
        if seconds <= 0:
            return "0s"
        
        total_seconds = int(seconds)
        hours = total_seconds // 3600
        minutes = (total_seconds % 3600) // 60
        secs = total_seconds % 60
        
        if hours > 0:
            return f"{hours}h{minutes:02d}m"
        elif minutes > 0:
            return f"{minutes}m{secs:02d}s"
        else:
            return f"{secs}s"
    
    def _save_processing_results(self, batch_result: BatchProcessingResult) -> None:
        """
        Save successful processing results to Excel files.
        
        Args:
            batch_result (BatchProcessingResult): Results from batch processing
        """
        if not batch_result.results:
            return
        
        print(f"\n\nSaving {batch_result.successful_combinations} successful results...")
        
        saved_files = {}
        method_names = self.preprocessor_registry.get_method_names()
        
        for combination_id, result in batch_result.results.items():
            if result.success and result.processed_data is not None:
                try:
                    # Generate method names for filename
                    combination_method_names = [
                        method_names.get(mid, f"Method{mid}") 
                        for mid in result.combination
                    ]
                    
                    # Get output file path
                    output_path = self.output_manager.get_full_output_path(
                        directory=self.current_config.output_directory,
                        combination_id=combination_id,
                        method_names=combination_method_names
                    )
                    
                    # Save the file
                    success = self.output_manager.save_excel(result.processed_data, output_path)
                    saved_files[output_path] = success
                    
                    if success:
                        print(f"✓ Saved: {Path(output_path).name}")
                    else:
                        print(f"✗ Failed: {Path(output_path).name}")
                        
                except Exception as e:
                    print(f"✗ Error saving {combination_id}: {e}")
                    saved_files[f"error_{combination_id}"] = False
        
        # Generate and save summary report
        try:
            summary_report = self.output_manager.generate_summary_report(saved_files)
            summary_path = Path(self.current_config.output_directory) / "processing_summary.txt"
            
            with open(summary_path, 'w') as f:
                f.write(summary_report)
            
            print(f"✓ Summary report saved: {summary_path}")
            
        except Exception as e:
            print(f"Warning: Could not save summary report: {e}")
    
    def _display_processing_results(self, batch_result: BatchProcessingResult, 
                                  progress_tracker: ProgressTracker) -> None:
        """
        Display final processing results and summary.
        
        Args:
            batch_result (BatchProcessingResult): Results from batch processing
            progress_tracker (ProgressTracker): Progress tracking information
            
        Requirements: 7.4 - results summary display with file locations
        """
        print("\n\n" + "=" * 60)
        print("PROCESSING RESULTS")
        print("=" * 60)
        
        # Overall statistics
        print(f"Total combinations: {batch_result.total_combinations}")
        print(f"Successful: {batch_result.successful_combinations}")
        print(f"Failed: {batch_result.failed_combinations}")
        print(f"Success rate: {batch_result.success_rate:.1f}%")
        print(f"Total processing time: {self._format_time_duration(batch_result.total_processing_time)}")
        
        if batch_result.cancelled:
            print("Status: CANCELLED")
        else:
            print("Status: COMPLETED")
        
        # Performance metrics
        if batch_result.total_combinations > 0:
            avg_time = batch_result.total_processing_time / batch_result.total_combinations
            print(f"Average time per combination: {avg_time:.3f}s")
        
        # Output directory information
        print(f"\nOutput directory: {self.current_config.output_directory}")
        
        # List successful files
        if batch_result.successful_combinations > 0:
            print(f"\n✓ {batch_result.successful_combinations} files generated successfully")
            
            # Show first few successful files
            successful_files = [
                result for result in batch_result.results.values() 
                if result.success
            ]
            
            display_count = min(5, len(successful_files))
            print(f"Sample files (showing {display_count} of {len(successful_files)}):")
            
            for i, result in enumerate(successful_files[:display_count]):
                method_names = self.preprocessor_registry.get_method_names()
                combination_desc = " → ".join([
                    method_names.get(mid, f"Method{mid}") 
                    for mid in result.combination
                ])
                print(f"  {i+1}. {result.combination_id}: {combination_desc}")
        
        # List failed combinations
        if batch_result.failed_combinations > 0:
            print(f"\n✗ {batch_result.failed_combinations} combinations failed")
            
            failed_results = [
                result for result in batch_result.results.values() 
                if not result.success
            ]
            
            display_count = min(3, len(failed_results))
            print(f"Failed combinations (showing {display_count} of {len(failed_results)}):")
            
            for i, result in enumerate(failed_results[:display_count]):
                print(f"  {i+1}. {result.combination_id}: {result.error_message}")
        
        print("\n" + "=" * 60)
        
        # Offer to open output directory
        self._offer_open_output_directory()
    
    def _offer_open_output_directory(self) -> None:
        """Offer to open the output directory in file explorer."""
        if self.current_config and self.current_config.output_directory:
            try:
                response = input("\nWould you like to open the output directory? (y/n): ").strip().lower()
                if response in ['y', 'yes']:
                    output_path = Path(self.current_config.output_directory)
                    if output_path.exists():
                        # Try to open directory in file explorer
                        if sys.platform == "win32":
                            os.startfile(output_path)
                        elif sys.platform == "darwin":  # macOS
                            os.system(f"open '{output_path}'")
                        else:  # Linux
                            os.system(f"xdg-open '{output_path}'")
                        print("Output directory opened.")
                    else:
                        print("Output directory not found.")
            except Exception as e:
                print(f"Could not open directory: {e}")
    
    def _display_system_info(self) -> None:
        """Display system and component information."""
        print("\n" + "=" * 50)
        print("SYSTEM INFORMATION")
        print("=" * 50)
        
        # Preprocessor registry info
        registry_info = self.processing_pipeline.get_registry_info()
        print(f"Available preprocessing methods: {registry_info['processor_count']}")
        
        method_names = registry_info['method_names']
        for method_id in sorted(method_names.keys()):
            print(f"  {method_id}: {method_names[method_id]}")
        
        # System status
        print(f"\nCurrent status:")
        print(f"  Data loaded: {'Yes' if self.current_data is not None else 'No'}")
        print(f"  Configuration set: {'Yes' if self.current_config is not None else 'No'}")
        
        if self.current_data is not None:
            print(f"  Data dimensions: {self.current_data.shape}")
        
        if self.current_config is not None:
            total_combinations = self.combination_generator.calculate_total_combinations(
                self.current_config.layer_selections
            )
            print(f"  Total combinations: {total_combinations}")
        
        print()
    
    def _display_help(self) -> None:
        """Display help information."""
        print("\n" + "=" * 50)
        print("HELP")
        print("=" * 50)
        
        help_text = """
WORKFLOW:
1. Select Excel File: Choose your input data file
   - File must be in .xlsx or .xls format
   - First column should contain IDs or labels
   - Remaining columns should contain numeric data

2. Configure Processing: Set up preprocessing layers
   - Choose number of layers (1-10)
   - Select preprocessing methods for each layer
   - System will calculate total combinations

3. Start Processing: Execute the preprocessing pipeline
   - All combinations will be processed
   - Progress will be displayed in real-time
   - Results will be saved to separate Excel files

PREPROCESSING METHODS:
- Method 0: No processing (default, always available)
- Additional methods depend on installed preprocessors

KEYBOARD SHORTCUTS:
- Ctrl+C: Cancel current operation
- Enter: Use default values where applicable

OUTPUT:
- Each combination generates a separate Excel file
- Files are organized in timestamped directories
- Summary report is generated with processing details

TIPS:
- Start with fewer layers/methods for testing
- Large combination counts (>1000) may take significant time
- Output files are named descriptively based on methods used
        """
        
        print(help_text)
        input("\nPress Enter to continue...")
    
    def _confirm_exit(self) -> bool:
        """
        Confirm exit with user.
        
        Returns:
            bool: True if user confirms exit, False otherwise
        """
        try:
            response = input("\nAre you sure you want to exit? (y/n): ").strip().lower()
            return response in ['y', 'yes']
        except:
            return True
    
    def _handle_exit(self) -> None:
        """Handle application exit."""
        print("\nThank you for using the Data Preprocessing Pipeline!")
        print("Goodbye!")
    
    def _cleanup(self) -> None:
        """Clean up resources before exit."""
        try:
            if hasattr(self, 'file_manager'):
                self.file_manager.cleanup()
        except Exception as e:
            print(f"Warning: Cleanup error: {e}")


def main():
    """Main entry point for the console UI."""
    try:
        ui = ConsoleUI()
        ui.run()
    except KeyboardInterrupt:
        print("\n\nApplication interrupted by user.")
    except Exception as e:
        print(f"Critical error: {e}")
        print("Please contact support if this problem persists.")


if __name__ == "__main__":
    main()